/**
 * 
 */
package mas;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.Iterator;
import java.util.List;
import java.util.Map;
import java.util.Queue;
import java.util.Set;
import java.util.Map.Entry;
import java.util.concurrent.locks.ReentrantLock;

import javax.annotation.Nullable;

import org.apache.commons.math3.random.RandomGenerator;

import com.google.common.base.Optional;

import rinde.sim.core.SimulatorAPI;
import rinde.sim.core.TimeLapse;
import rinde.sim.core.graph.Point;
import rinde.sim.core.model.communication.CommunicationAPI;
import rinde.sim.core.model.communication.CommunicationUser;
import rinde.sim.core.model.communication.Mailbox;
import rinde.sim.core.model.communication.Message;
import rinde.sim.core.model.pdp.PDPModel;
import rinde.sim.core.model.pdp.Vehicle;
import rinde.sim.core.model.road.MoveProgress;
import rinde.sim.core.model.road.RoadModel;
import rinde.sim.core.model.road.RoadUser;

/**
 * @author IVAN
 * @description This class represents a generic agent, whether a master or a slave
 */
public abstract class Truck extends Vehicle implements CommunicationUser{

	/**
	 * @description Each truck can handle one single box at a time
	 */
	//the next box the slave has to go to after the current delivery (unless his Master changes the plans)
	protected Optional<Box> pendingBox = Optional.absent();
	protected boolean occupied;
	/**
	 * @description Truck capacity is always one single box, box dimensions don't matter
	 */
	//protected Box box;
	protected int agentNumber;
	protected Point location;
	protected double SPEED = 3000d; 
	protected Optional<RoadModel> roadModel;
	protected Optional<PDPModel> pdpModel;
	protected boolean waiting;
	protected Optional<Box> curr;
	protected int capacity = 10; //maximum boxes per truck (including pending boxes and current box) Default=1 (no pending boxes)
	private final int radius;
	private long lastCommunication;
	private final double reliability;
	protected ArrayList<Box> myAssignedBoxes;
	private static final int MAX_MSGs = 2000;
	// 10s
	private static final int COMMUNICATION_PERIOD = 10000;
	protected CommunicationAPI cm;
	Map<Truck, Long> lastCommunicationTime;
	Map<Truck, ArrayList<Integer>> messagesSpread;
	private Set<Truck> communicatedWith;
	private final Mailbox mailbox;
	private final ReentrantLock lock;
	private int communications;
	//for annotating the messages with
	//keeping this private instead of protected makes sure this is always accessed with the getAndRaiseCounter method, which is what we want
	private int counter = 0;

	//note that this doesn't set any location, which should eventually be done, but it does set
	//the road- and pdpmodel
	public Truck(int radius, double reliability, RoadModel rm, PDPModel pm) {
		super();
		this.setOccupied(false);
		//this.setCapacity(1);
		this.radius=radius;
		this.reliability=reliability;
		this.waiting = false;
		roadModel = Optional.of(rm);
		pdpModel = Optional.of(pm);
		curr = Optional.absent();
		communicatedWith = new HashSet<Truck>();
		lastCommunicationTime = new HashMap<Truck, Long>();
		messagesSpread = new HashMap<Truck, ArrayList<Integer>>();
		mailbox = new Mailbox();
		lock = new ReentrantLock();
		communications = 0;
		myAssignedBoxes = new ArrayList<Box>();
	}

	public boolean isAtFullCapacity(){
		int currentload = 0;
		if (this.isOccupied()) currentload++;
		currentload += this.myAssignedBoxes.size();
		return currentload >= this.capacity;
	}

	public int getAndRaiseCounter(){
		int currentValue = this.counter;
		this.counter= this.counter + 1;
		return currentValue;
	}

	public int getAgentNumber() {
		return agentNumber;
	}

	public void setAgentNumber(int agentNumber) {
		this.agentNumber = agentNumber;
	}

	public boolean isWaiting() {
		return waiting;
	}
	public void setWaiting(boolean waiting) {
		this.waiting = waiting;
	}
	public Point getLocation() {
		return location;
	}

	//when it is not the start. so setstartposition should actually be in the constructor, but our current
	//initlialisation of the app makes the masters first before their location is calculated
	public void setLocation(Point location) {
		this.location = location;
		this.setStartPosition(location);
	}

	public boolean isOccupied() {
		return occupied;
	}
	public void setOccupied(boolean occupied) {
		this.occupied = occupied;
	}

	public Optional<Box> getBox() {
		return curr;
	}
	public boolean setBox(Box box) {
		boolean assigned = false;
		if (!this.occupied){
			this.curr = Optional.of(box);
			this.setOccupied(true);			
			assigned= true;
		}else{ 
			//if there isn't a pending box and not repeated
			if (!this.isAtFullCapacity()){
				this.myAssignedBoxes.add(box);
				assigned= true;
			}
			else{
				this.reQueueBox(box);
			}
		}
		if(assigned){
			//this.setWaiting(false);
		}
		return assigned;
	}

	
	abstract protected void reQueueBox(Box box);
	abstract protected void markToRemove(Box box);
	
	public double getSpeed() {
		return SPEED;
	}

	@Override
	public void afterTick(TimeLapse timeLapse) {}

	@Override
	protected void tickImpl(TimeLapse time){
		checkMsgs(time.getTime());
		refreshList(time.getTime());
		final RoadModel rm = roadModel.get();
		final PDPModel pm = pdpModel.get();

		if (!time.hasTimeLeft()) {
			return;
		}
		if (!curr.isPresent()) {
			curr = Optional.absent(); //pendingBox;
		}
		if (curr.isPresent()) {
			final boolean inCargo = pm.containerContains(this, curr.get());
			// sanity check: if it is not in our cargo AND it is also not on the
			// RoadModel, we cannot go to curr anymore.
			if (!inCargo && !rm.containsObject(curr.get())) {
				curr = Optional.absent();
				this.setOccupied(false);
			} else if (inCargo) {
				// if it is in cargo, go to its destination
				MoveProgress mp = rm.moveTo(this, curr.get().getDestination(), time);
				App.totalTraveledDistance += mp.distance.getValue().longValue();
				if ((rm.getPosition(this).equals(curr.get().getDestination())) && (time.getTime()>=curr.get().getDeliveryTimeWindow().begin)) {
					// deliver when we arrive
					pm.deliver(this, curr.get(), time);
					curr.get().setDelivered(true);
					App.deliveredBoxes[App.totalDeliveredBoxes] = curr.get().getBoxNumber(); 
					App.totalDeliveredBoxes++;
					App.deliveryError += (time.getTime() - curr.get().getDeliveryTimeWindow().begin);
					System.out.println("Box-"+curr.get().getBoxNumber()+" was delivered("+App.totalDeliveredBoxes+"/"+App.totalBoxes+") at time:" + time.getTime() + " , within time window "+ curr.get().getDeliveryTimeWindow().begin + " and " + curr.get().getDeliveryTimeWindow().end);
					//if more pending boxes
					if (this.myAssignedBoxes.size() > 0){
						this.curr = Optional.of(this.myAssignedBoxes.get(0));
						this.myAssignedBoxes.remove(0);
					}else{
						this.setOccupied(false); //set free once box is delivered
					}

				}
			} else {
				// it is still available, go there as fast as possible
//				if(curr.get().isAttended()){
//					curr=Optional.absent();
//				}
				MoveProgress mp = rm.moveTo(this, curr.get(), time);
				App.totalTraveledDistance += mp.distance.getValue().longValue();
				if ((rm.equalPosition(this, curr.get())) && (time.getTime()>=curr.get().getPickupTimeWindow().begin)) {
					// pickup box
					pm.pickup(this, curr.get(), time);
					//curr.get().setAttended(true);
					App.pickupError += (time.getTime() - curr.get().getPickupTimeWindow().begin);
				}
			}
		}
		else{
			if(this instanceof Master){
				MoveProgress mp = rm.moveTo(this, App.getPointToGoToWhenIdle((Master) this), time);
				App.totalTraveledDistance += mp.distance.getValue().longValue();
			}
			else if(this instanceof Slave){
				MoveProgress mp = rm.moveTo(this, App.getPointToGoToWhenIdle((Slave) this), time);
				App.totalTraveledDistance += mp.distance.getValue().longValue();
			}
		}
		sendMsgs(time.getStartTime());
	}

	@Override
	public void initRoadPDP(RoadModel pRoadModel, PDPModel pPdpModel) {
		roadModel = Optional.of(pRoadModel);
		pdpModel = Optional.of(pPdpModel);
	}

	/**
	 * Calculate estimated time to pickup a new box
	 * @param Box b
	 * @return
	 */
	public long reportTime(Box b) {
		//current position in map
		Point myposition = this.getPosition();
		long totaltime = 0;
		double totalDistance = 0;
		if (this.occupied){ //if already has a box assigned, add up time to deliver the current box
			final PDPModel pm = pdpModel.get();
			//if already the current box was picked up
			final boolean inCargo = pm.containerContains(this, curr.get());
			if (inCargo){ //time to deliver current box, plus time to pickup next
				totalDistance += this.getRealDistance(this.getBox().get().getDestination(),myposition);
				totalDistance += this.getRealDistance(this.getBox().get().getDestination(), b.getpPickup());
				totaltime += this.getBox().get().getPickupDuration()*App.TIME_MULTIPLIER;
			}else{
				//time to pick up current box, 
				//plus time deliver current box, plus time to pickup next
				totalDistance += this.getRealDistance(this.getBox().get().getpPickup(),myposition);
				totalDistance += this.getRealDistance(this.getBox().get().getpPickup(),this.getBox().get().getDestination());
				totalDistance += this.getRealDistance(this.getBox().get().getDestination(), b.getpPickup());
				totaltime += this.getBox().get().getPickupDuration()*App.TIME_MULTIPLIER;
				totaltime += this.getBox().get().getDeliveryDuration()*App.TIME_MULTIPLIER;
			}
		}else{
			totalDistance = this.getRealDistance(b.getpPickup(), this.getPosition());
		}
		//estimated time
		double real_estimated_speed = SPEED/(32*App.TIME_MULTIPLIER);
		totaltime += Math.round((totalDistance / real_estimated_speed)); //time in miliseconds
		return totaltime;
	}

	protected long reportPickupTime(Box b){
		//estimated time
		double real_estimated_speed = SPEED/(32*App.TIME_MULTIPLIER);
		long totalDistance = this.getRealDistance(b.getpPickup(), this.getPosition());
		return Math.round((totalDistance / real_estimated_speed));
	}
	
	
	private void refreshList(long currentTime) {
		if (lastCommunication + COMMUNICATION_PERIOD < currentTime) {
			lock.lock();
			communicatedWith = new HashSet<Truck>();
			for (final Entry<Truck, Long> e : lastCommunicationTime
					.entrySet()) {
				if (e.getValue() + COMMUNICATION_PERIOD * 100 >= currentTime) {
					communicatedWith.add(e.getKey());
				}
			}
			lock.unlock();
		}
	}

	private void sendMsgs(long currentTime) {
		//System.out.println("now in sendMsgs");
		if (lastCommunication + COMMUNICATION_PERIOD < currentTime) {
			//System.out.println("now communication");
			lastCommunication = currentTime;
			if (cm != null) {
				//System.out.println("cm is not null");
				//cm.broadcast(new Message(this) {});
			}
		}
	}

	/**
	 * Returns real distance between 2 points, based on the shortest path found
	 * @return
	 */
	private long getRealDistance(Point p1, Point p2){
		RoadModel rm = roadModel.get();
		List<Point> intersections = rm.getShortestPathTo(p1, p2);
		long distance = 0;
		int i = 0;

		while(i<intersections.size()-1){
			distance += Point.distance(intersections.get(i),intersections.get(i+1));
			i+=1;
		}
		return distance;
	}

	private void checkMsgs(long currentTime) {
		final Queue<Message> messages = mailbox.getMessages();
		for (final Message m : messages) {
			lastCommunicationTime.put((Truck) m.getSender(), currentTime);
			if(m instanceof BoxMessage){
				doAction((BoxMessage) m);
			}
			if(m instanceof AcknowledgementMessage){
				doAction((AcknowledgementMessage) m);
			}
			communications++;
		}
	}

	public void doAction(BoxMessage message){
		if(!(this.messagesSpread.containsKey(message.getSender()))){
			messagesSpread.put((Truck) message.getSender(), new ArrayList<Integer>());
		}
		ArrayList<Integer> spreadMessagesOfSender = this.messagesSpread.get(message.getSender());
		int counterOfMessage= message.getCounter();
		if(!(spreadMessagesOfSender.contains(counterOfMessage))){
			cm.broadcast(message);
			spreadMessagesOfSender.add(counterOfMessage);	
		}
	}
	
	public void doAction(AcknowledgementMessage message){
		if(!(this.messagesSpread.containsKey(message.getSender()))){
			messagesSpread.put((Truck) message.getSender(), new ArrayList<Integer>());
		}
		ArrayList<Integer> spreadMessagesOfSender = this.messagesSpread.get(message.getSender());
		int counterOfMessage= message.getCounter();
		if(!(spreadMessagesOfSender.contains(counterOfMessage))){
			cm.broadcast(message);
			spreadMessagesOfSender.add(counterOfMessage);	
		}
	}

	public void doAction(Message message){
		System.out.println("This should not be printed, method is overloaded with method for every subtype.");
	}

	public void setCommunicationAPI(CommunicationAPI api) {
		cm = api;
	}

	public Point getPosition() {
		return roadModel.get().getPosition(this);
	}

	public double getRadius() {
		return this.radius;
	}

	public double getReliability() {
		return this.reliability;
	}

	public void receive(Message message) {
		mailbox.receive(message);		
	}	

	public int getNoReceived() {
		return communications;
	}
	
	public Set<Truck> getCommunicatedWith() {
		lock.lock();
		final Set<Truck> result = new HashSet<Truck>(
				communicatedWith);
		lock.unlock();
		return result;
	}
}
